// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:build/build.dart';

/// Overridable behavior for a [Builder.build] method.
typedef BuildBehavior =
    FutureOr Function(
      BuildStep buildStep,
      Map<String, List<String>> buildExtensions,
    );

/// Copy the input asset to all possible output assets.
Future<void> _defaultBehavior(
  BuildStep buildStep,
  Map<String, List<String>> buildExtensions,
) => _copyToAll(buildStep, buildExtensions);

T _identity<T>(T value) => value;
Future<String> _readAsset(BuildStep buildStep, AssetId assetId) =>
    buildStep.readAsString(assetId);

/// Pass the input assetId through [readFrom] and duplicate the results of
/// [read] on that asset into every matching output based on [buildExtensions].
Future<void> _copyToAll(
  BuildStep buildStep,
  Map<String, List<String>> buildExtensions, {
  AssetId Function(AssetId assetId) readFrom = _identity,
  Future<String> Function(BuildStep buildStep, AssetId assetId) read =
      _readAsset,
}) {
  final outputs = buildStep.allowedOutputs;
  if (outputs.isEmpty) {
    // If there's no output, the builder shouldn't run.
    throw ArgumentError(
      'Only expected inputs with extension in '
      '${buildExtensions.keys.toList()} but got ${buildStep.inputId}',
    );
  }

  return Future.wait(
    outputs.map(
      (id) => buildStep.writeAsString(
        id,
        read(buildStep, readFrom(buildStep.inputId)),
      ),
    ),
  );
}

/// A build behavior which reads [assetId] and copies it's content into every
/// output.
BuildBehavior copyFrom(AssetId assetId) =>
    (buildStep, buildExtensions) =>
        _copyToAll(buildStep, buildExtensions, readFrom: (_) => assetId);

/// A build behavior which writes either 'true' or 'false' depending on whether
/// [assetId] can be read.
BuildBehavior writeCanRead(AssetId assetId) =>
    (
      BuildStep buildStep,
      Map<String, List<String>> buildExtensions,
    ) => _copyToAll(
      buildStep,
      buildExtensions,
      readFrom: (_) => assetId,
      read: (buildStep, assetId) async => '${await buildStep.canRead(assetId)}',
    );

/// A [Builder.buildExtensions] which operats on assets ending in [from] and
/// creates outputs with [postFix] appended as the extension.
///
/// If [numCopies] is greater than 1 the postFix will also get a `.0`, `.1`...
Map<String, List<String>> appendExtension(
  String postFix, {
  String from = '',
  int numCopies = 1,
}) => {
  from:
      numCopies == 1
          ? ['$from$postFix']
          : List.generate(numCopies, (i) => '$from$postFix.$i'),
};

Map<String, List<String>> replaceExtension(String from, String to) => {
  from: [to],
};

/// A test [Builder].
///
/// Runs for all inputs and write outputs with  `.copy` appended. Or, pass
/// [buildExtensions] to specify input and output extensions.
///
/// Copy its input to all possible outputs. Pass `build` to replace this
/// behavior, and/or `extraWork` to add additional behavior.
///
/// Default name for logging and for `build.yaml` is `TestBuilder`. Pass
/// `name` to set the name.
class TestBuilder implements Builder {
  @override
  final Map<String, List<String>> buildExtensions;

  final String name;
  final BuildBehavior _build;
  final BuildBehavior? _extraWork;

  /// A stream of all the [BuildStep.inputId]s that are seen.
  ///
  /// Events are added at the start of the [build] method.
  final _buildInputsController = StreamController<AssetId>.broadcast();
  Stream<AssetId> get buildInputs => _buildInputsController.stream;

  /// A stream of all the [BuildStep.inputId]s that are completed.
  ///
  /// Events are added at the end of the [build] method.
  final _buildsCompletedController = StreamController<AssetId>.broadcast();
  Stream<AssetId> get buildsCompleted => _buildsCompletedController.stream;

  TestBuilder({
    Map<String, List<String>>? buildExtensions,
    BuildBehavior? build,
    BuildBehavior? extraWork,
    this.name = 'TestBuilder',
  }) : buildExtensions = buildExtensions ?? appendExtension('.copy'),
       _build = build ?? _defaultBehavior,
       _extraWork = extraWork;

  @override
  Future build(BuildStep buildStep) async {
    if (!await buildStep.canRead(buildStep.inputId)) return;
    _buildInputsController.add(buildStep.inputId);
    await _build(buildStep, buildExtensions);
    await _extraWork?.call(buildStep, buildExtensions);
    _buildsCompletedController.add(buildStep.inputId);
  }

  @override
  String toString() => name;
}
